package com.streambolics.std;

import java.util.Calendar;

/**
 * A way to create an HTML document.
 * <p>
 * The HtmlBuilder object is somewhat reminiscent of the StringBuilder object,
 * as it allows to create an HTML document one element at a time.
 * </p>
 * <p>
 * HtmlBuilder is not meant to be a framework that allows any HTML document to
 * be created and/or manipulated. To do so, use a DOM editor instead.
 * HtmlBuilder focuses on a set of highly cohesive set of primitive constructs,
 * such as a framed form, and a simple list implemented as a table.
 * </p>
 * 
 * @author Sauron
 * 
 */

public class HtmlBuilder implements TextBuilder, HtmlCapabilities
{
    private final StringBuilder _Body;
    private final StringBuilder _Head;

    private final StringBuilder _Init;
    private String              _Title;

    private int                 _RequiredTableCols;

    private int                 _OptionalTableCols;

    private int                 _CellNumber;

    private HtmlCapabilities    _Capabilities;

    public HtmlBuilder()
    {
        _Body = new StringBuilder ();
        _Head = new StringBuilder ();
        _Init = new StringBuilder ();
    }

    private static void add (StringBuilder aBuilder, String... aStrings)
    {
        for (String s : aStrings)
        {
            aBuilder.append (s);
        }
    }

    private static void addTag (StringBuilder aBuilder, String aName, boolean aClosed, String... aNameValuePairs)
    {
        boolean isValue = false;
        add (aBuilder, "<", aName);
        for (String s : aNameValuePairs)
        {
            if (isValue)
            {
                add (aBuilder, "=\"", s, "\"");
            }
            else
            {
                add (aBuilder, " ", s);
            }
            isValue = !isValue;
        }
        if (aClosed)
        {
            aBuilder.append ("/>\n");
        }
        else
        {
            aBuilder.append (">");
        }
    }

    /**
     * Adds a complete form allowing the user to modify a street address.
     * <p>
     * The form is well suited to modify the address of an Agent object.
     * </p>
     * 
     * @param aURL
     *            The URL to call when the user presses the OK button.
     * @param aLat
     *            The current latitude of the address. The modified latitude
     *            will be encoded in the LATITUDE parameter of the request.
     * @param aLng
     *            The current longitude of the address. The modified longitude
     *            will be encoded in the LONGITUDE parameter of the request.
     * @param address
     *            The text of the address. The modified address will be encoded
     *            in the ADDRESS parameter of the request.
     * @param aIdString
     *            An identification of the object owning the address. This id
     *            will be encoded in the ID parameter of the request.
     */

    public void addAddressForm (String aURL, double aLat, double aLng, String address, String aIdString)
    {
        openForm (aURL);
        addHidden ("ID", aIdString);
        addGoogleMap (aLat, aLng, address);
        addInput ("hidden", "LATITUDE", Double.toString (aLat), " id=\"ggmap_lat\"");
        addInput ("hidden", "LONGITUDE", Double.toString (aLat), " id=\"ggmap_lng\"");
        addLabel ("ADDRESS", "normal", "Address: ");
        addInput ("text", "ADDRESS", address, " id=\"ggmap_adr\"");
        addSubmitButton ("OK", "Change");
        closeForm ();
    }

    /**
     * Adds an anchor.
     * 
     * @param aURL
     *            The URL to link to.
     * @param aText
     *            The text to display.
     */

    @Override
    public void addAnchor (String aURL, String aText)
    {
        openAnchor (aURL);
        addText (aText);
        closeAnchor ();
    }

    /**
     * Adds a break in the text.
     */

    public void addBreak ()
    {
        closeElement ("<br/>");
    }

    /**
     * Adds a check box.
     * 
     * @param aName
     *            The name of the checkbox. Usually in full uppercase, it will
     *            be encoded as the name of the parameter in the request. If the
     *            box is checked, the value of the parameter will be "yes".
     * @param aChecked
     *            Whether the checkbox is initially checked.
     */

    public void addCheckBox (String aName, boolean aChecked)
    {
        addInput ("checkbox", aName, "yes", aChecked ? " checked" : "");
    }

    /**
     * Adds a choice for a combo box.
     * <p>
     * This call is only valid within openCombobox / closeCombobox.
     * </p>
     * 
     * @param aValue
     *            The value that will be encoded in the request if this option
     *            is chosen.
     * @param aText
     *            The text of the option as displayed to the user.
     * @param aDefault
     *            Whether this value is currently selected. Behaviour when
     *            multiple options are selected is undefined.
     */

    public void addComboChoice (int aValue, String aText, boolean aDefault)
    {
        addComboChoice (Integer.toString (aValue), aText, aDefault);
    }

    public void addComboChoice (int aValue, String aText, int aDefault)
    {
        addComboChoice (aValue, aText, aValue == aDefault);
    }

    public void addComboChoice (String aValue, String aText)
    {
        addComboChoice (aValue, aText, false);
    }

    public void addComboChoice (String aValue, String aText, boolean aDefault)
    {
        addTag (_Body, "option", false, "value", aValue, aDefault ? "selected" : "");
        addText (aText);
        closeElement ("</option>");
    }

    public void addComboChoice (String aValue, String aText, String aDefault)
    {
        addComboChoice (aValue, aText, aValue.equals (aDefault));
    }

    /**
     * Adds a set of combobox choices.
     * 
     * @param aValues
     *            The choice values. All strings in the array are used both as
     *            internal value and display text.
     * @param aDefault
     *            The default value. If one of the string in aValues matches
     *            this one, it will be selected by default.
     */

    public void addComboChoices (String[] aValues, String aDefault)
    {
        for (String s : aValues)
        {
            addComboChoice (s, s, aDefault);
        }
    }

    /**
     * Adds a field allowing the user to modify a date.
     * 
     * @param aName
     *            The name of the fields. The selected values will be encoded in
     *            three parameters beginning with that name and suffixes _DY,
     *            _MO and _YR.
     * @param aPrompt
     * @param aMinYear
     * @param aMaxYear
     * @param aValue
     */

    public void addDateField (String aName, String aPrompt, int aMinYear, int aMaxYear, Calendar aValue)
    {
        addLabel (aName + "_DY", "day", aPrompt);
        addDayCombobox (aName + "_DY", aValue);
        addLabel (aName + "_MO", "month", "/");
        addMonthCombobox (aName + "_MO", aValue);
        addLabel (aName + "_YR", "year", "/");
        addYearCombobox (aName + "_YR", aMinYear, aMaxYear, aValue);
    }

    /**
     * Adds combo box to select a day.
     * 
     * @param aName
     * @param aValue
     */

    public void addDayCombobox (String aName, Calendar aValue)
    {
        if (hasSpinBoxes ())
        {

        }
        else
        {
            // TODO : use some magic to make a "true" month calendar to pop up,
            // instead of a boring list.

            int v = openCalendarComboBox (aName, "day", aValue, Calendar.DAY_OF_MONTH);
            addNumericComboChoices (1, 31, v);
            closeCombobox ();
        }
    }

    /**
     * Adds a clickable Google Maps control.
     * 
     * <p>
     * Note that for simplification reasons, only one map can be put on a page.
     * </p>
     * 
     * @param aLat
     *            The latitude to use as center
     * @param aLng
     *            The longitude to use as center
     * @param mark
     *            Whether to put a marker also.
     */

    public void addGoogleMap (double aLat, double aLng, String mark)
    {
        openElementEx ("div", "id", "ggmap");
        closeElement ("</div>");
        includeJs ("http://maps.google.com/maps/api/js?sensor=false");
        includeJs ("/js/ggmaptools.js");
        addInitLine (String.format ("ggmap_create (new google.maps.LatLng(%f, %f), %d);", aLat, aLng, (aLat == 0 && aLng == 0) ? 3 : 10));
        if (mark != null && aLat != 0 && aLng != 0)
        {
            addInitLine (String.format ("ggmap_mark (new google.maps.LatLng(%f, %f), \"%s\");", aLat, aLng, mark));
        }
    }

    /**
     * Add a hidden input box.
     * 
     * @param aName
     *            The name of the control. Will be returned in the URL.
     * @param aValue
     *            The value to be returned on submit.
     */

    public void addHidden (String aName, String aValue)
    {
        addInput ("hidden", aName, aValue);
    }

    private void addHourCombobox (String aName, Calendar aValue)
    {
        int v = openCalendarComboBox (aName, "hour", aValue, Calendar.HOUR_OF_DAY);
        addNumericComboChoices (0, 23, v);
        closeCombobox ();
    }

    /**
     * Add a javascript statement to the initialization routine.
     * 
     * @param aLine
     *            The Javascript statement to add.
     */

    public void addInitLine (String aLine)
    {
        add (_Init, "  ", aLine, "\n");
    }

    /**
     * Add an input to the current form.
     * 
     * @param aType
     *            The type of control.
     * @param aName
     *            The name of the control.
     * @param aValue
     *            The initial value.
     */

    public void addInput (String aType, String aName, String aValue)
    {
        addInput (aType, aName, aValue, "");
    }

    public void addInput (String aType, String aName, String aValue, String aOpts)
    {
        addTag (_Body, "input", true, "type", aType, "name", aName, "value", aValue == null ? "" : aValue, aOpts);
    }

    public void addLabel (String aName, String aClass, String aValue)
    {
        addTag (_Body, "label", false, "class", aClass, "for", aName);
        addText (aValue);
        closeElement ("</label>");
    }

    /**
     * 
     * @param aUrl
     * @param aText
     * @obsolete Use addAnchor instead.
     */

    public void addLink (String aUrl, String aText)
    {
        addTag (_Body, "a", false, "href", escapeUrl (aUrl));
        addText (aText);
        closeElement ("</a>");
    }

    public void addMetaName (String aName, String aContent)
    {
        addTag (_Head, "meta", true, "name", aName, "content", aContent);
    }

    private void addMinuteCombobox (String aName, Calendar aValue)
    {
        int v = openCalendarComboBox (aName, "minute", aValue, Calendar.MINUTE);
        addNumericComboChoices (0, 59, 5, v);
        closeCombobox ();
    }

    public void addMonthCombobox (String aName, Calendar aValue)
    {
        int d = openCalendarComboBox (aName, "month", aValue, Calendar.MONTH) + 1;
        addMonthsComboChoices (d);
        closeCombobox ();
    }

    public void addMonthsComboChoices (int aDefault)
    {
        addComboChoice (-1, "", aDefault);
        addComboChoice (1, "Jan", aDefault);
        addComboChoice (2, "Feb", aDefault);
        addComboChoice (3, "Mar", aDefault);
        addComboChoice (4, "Apr", aDefault);
        addComboChoice (5, "May", aDefault);
        addComboChoice (6, "Jun", aDefault);
        addComboChoice (7, "Jul", aDefault);
        addComboChoice (8, "Aug", aDefault);
        addComboChoice (9, "Sep", aDefault);
        addComboChoice (10, "Oct", aDefault);
        addComboChoice (11, "Nov", aDefault);
        addComboChoice (12, "Dec", aDefault);
    }

    public void addNumericComboChoices (int aMin, int aMax, int aDefault)
    {
        addNumericComboChoices (aMin, aMax, 1, aDefault);
    }

    public void addNumericComboChoices (int aMin, int aMax, int aStep, int aDefault)
    {
        addComboChoice (-1, "", aDefault);
        for (int i = aMin; i <= aMax; i += aStep)
        {
            addComboChoice (i, Integer.toString (i), i <= aDefault && aDefault < i + aStep);
        }
    }

    @Override
    public void addParagraph (String... aTexts)
    {
        openParagraph ();
        addText (aTexts);
        closeParagraph ();
    }

    public void addRule ()
    {
        closeElement ("<hr/>");
    }

    public void addSpan (String aStyle, String aText)
    {
        openSpan (aStyle);
        addText (aText);
        closeSpan ();
    }

    public void addStyleSheet (String aUrl)
    {
        addTag (_Head, "link", true, "rel", "stylesheet", "type", "text/css", "href", aUrl);
    }

    /**
     * Adds a button to the currently open form.
     * 
     * @param aCaption
     *            The caption to display on the button.
     * @param aName
     *            The name of the button.
     */

    public void addSubmitButton (String aName, String aCaption)
    {
        addInput ("submit", aName, aCaption);
    }

    @Override
    public void addText (String... aTexts)
    {
        for (String s : aTexts)
        {
            _Body.append (escapeString (s));
        }
    }

    public void addTextField (String aName, String aPrompt)
    {
        addTextField (aName, aPrompt, "");
    }

    public void addTextField (String aName, String aPrompt, int aLength)
    {
        addTextField (aName, aPrompt, "", "size=" + Integer.toString (aLength));
    }

    public void addTextField (String aName, String aPrompt, String aValue)
    {
        addTextField (aName, aPrompt, aValue, "");
    }

    public void addTextField (String aName, String aPrompt, String aValue, int aLength)
    {
        addTextField (aName, aPrompt, aValue, "size=" + Integer.toString (aLength));
    }

    public void addTextField (String aName, String aPrompt, String aValue, String aOpts)
    {
        addLabel (aName, "normal", aPrompt);
        addInput ("text", aName, aValue, aOpts);
    }

    public void addTimeField (String aName, String aPrompt, Calendar aValue)
    {
        addLabel (aName + "_HR", "hour", aPrompt);
        addHourCombobox (aName + "_HR", aValue);
        addLabel (aName + "_MI", "minute", ":");
        addMinuteCombobox (aName + "_MI", aValue);
    }

    public void addYearCombobox (String aName, int aMin, int aMax, Calendar aValue)
    {
        int v = openCalendarComboBox (aName, "year", aValue, Calendar.YEAR);
        addNumericComboChoices (aMin, aMax, v);
        closeCombobox ();
    }

    public void appendToHead (String s)
    {
        _Head.append (s);
    }

    /**
     * Close an anchor element.
     */

    public void closeAnchor ()
    {
        closeElement ("</a>");
    }

    public void closeCombobox ()
    {
        closeElement ("</select>");
    }

    /**
     * Close a DIV element.
     */

    public void closeDiv ()
    {
        closeElement ("</div>");
    }

    /**
     * Closes any element.
     * 
     * @param s
     *            The HTML text of the closing tag. Must include the brackets.
     */

    public void closeElement (String s)
    {
        openElement (s);
        newLine ();
    }

    /**
     * Closes a form element.
     */

    public void closeForm ()
    {
        closeElement ("</form>");
    }

    @Override
    public void closeParagraph ()
    {
        closeElement ("</p>");
    }

    /**
     * Closes a table row (TR) element
     */

    public void closeRow ()
    {
        closeElement ("</tr>");
    }

    public void closeSpan ()
    {
        closeElement ("</span>");
    }

    public void closeTable ()
    {
        closeElement ("</table>");

    }

    /**
     * Closes a table cell (TD) element
     */

    public void closeTableCell ()
    {
        closeElement ("</td>");
    }

    public void closeTableHeader ()
    {
        closeElement ("</th>");
    }

    /**
     * Escapes the input string so that "dangerous" characters are eliminated.
     * 
     * @param s
     *            The input string to escape.
     * @return The input string with the reserved characters escaped.
     */

    public String escapeString (String s)
    {
        return s.replaceAll ("<", "&lt;");
    }

    /**
     * Escapes an URL
     * 
     * @param s
     * @return
     */

    public String escapeUrl (String s)
    {
        return s;
    }

    public String getBody ()
    {
        if (_Body.length () == 0)
        {
            return "";
        }
        else
        {
            return getBodyStart () + _Body.toString () + getBodyEnd ();
        }
    }

    protected String getBodyEnd ()
    {
        return "</body>";
    }

    protected String getBodyStart ()
    {
        if (hasInit ())
        {
            return "<body onload=\"doinitscript()\">";
        }
        else
        {
            return "<body>";
        }
    }

    private HtmlCapabilities getCapabilities ()
    {
        if (_Capabilities == null)
        {
            _Capabilities = MinimalHtmlCapabilities.getSingleton ();
        }
        return _Capabilities;
    }

    private Object getHeadEnd ()
    {
        return "</head>\n";
    }

    private String getHeadStart ()
    {
        return "<head>";
    }

    /**
     * Get the full HTML output.
     * 
     * @return The string containing the full HTML markup.
     */

    public String getHtml ()
    {
        StringBuilder b = new StringBuilder ();
        b.append (getHtmlStart ());
        b.append (getHeadStart ());
        if (_Title != null)
        {
            b.append ("<title>");
            b.append (escapeString (_Title));
            b.append ("</title>\n");
        }
        b.append (_Head);
        if (hasInit ())
        {
            b.append ("<script type=\"text/javascript\">\nfunction doinitscript()\n{\n");
            b.append (_Init);
            b.append ("}\n</script>\n");
        }

        b.append (getHeadEnd ());
        b.append (getBodyStart ());
        b.append (_Body);
        b.append (getBodyEnd ());
        b.append (getHtmlEnd ());
        return b.toString ();
    }

    /**
     * The tag to add at the end of the HTML output.
     * 
     * @return An HTML fragment that ends the HTML generated by the object.
     */

    private String getHtmlEnd ()
    {
        return "</html>\n";
    }

    private String getHtmlStart ()
    {
        return "<!DOCTYPE html>\n<html>";
    }

    public String getTitle ()
    {
        return _Title;
    }

    @Override
    public boolean hasDatePicker ()
    {
        return getCapabilities ().hasDatePicker ();
    }

    public boolean hasInit ()
    {
        return _Init.length () > 0;
    }

    @Override
    public boolean hasSliders ()
    {
        return getCapabilities ().hasSliders ();
    }

    @Override
    public boolean hasSpinBoxes ()
    {
        return getCapabilities ().hasSpinBoxes ();
    }

    public void includeJs (String aURL)
    {
        addTag (_Head, "script", false, "type", "text/javascript", "src", aURL);
        _Head.append ("</script>\n");
    }

    public void newLine ()
    {
        _Body.append ("\n");
    }

    /**
     * A shortcut to CloseTableCell/OpenTableCell.
     * 
     */

    public void nextTableCell ()
    {
        closeTableCell ();
        openTableCell ();
    }

    /**
     * Opens an anchor (A)
     * 
     * @param aURL
     *            The URL to link to.
     */

    public void openAnchor (String aURL)
    {
        openElementEx ("a", "href", escapeUrl (aURL));
    }

    public int openCalendarComboBox (String aName, String aStyle, Calendar aValue, int aField)
    {
        _Body.append ("<select name=\"");
        _Body.append (aName);
        _Body.append ("\" class=\"");
        _Body.append (aStyle);
        _Body.append ("\" >");
        if (aValue == null)
        {
            return -1;
        }
        else
        {
            return aValue.get (aField);
        }
    }

    public void openCombobox (String aName, String aValue)
    {
        addTag (_Body, "select", false, "name", aName, "value", aValue);
    }

    /**
     * Opens a DIV element
     * 
     * @param aClass
     *            The class name indicating the style to apply to the text.
     */

    public void openDiv (String aClass)
    {
        openElementEx ("div", "class", aClass);
    }

    public void openElement (String s)
    {
        _Body.append (s);
    }

    protected void openElementEx (String aTag, String aProperty, String aValue)
    {
        addTag (_Body, aTag, false, aProperty, aValue);
    }

    /**
     * Opens a FORM element
     * 
     * @param aUrl
     *            The URL for the Commit.
     */

    public void openForm (String aUrl)
    {
        openElementEx ("form", "action", escapeUrl (aUrl));
    }

    /**
     * Opens a table cell (TD) that spans the entire table width.
     * 
     * The cell spans all columns (required + optional) of the table.
     */

    public void openFullTableCell ()
    {
        addTag (_Body, "td", false, "class", "full", "colspan", Integer.toString (_RequiredTableCols + _OptionalTableCols));
    }

    public void openFullTableHeader ()
    {
        addTag (_Body, "th", false, "class", "full", "colspan", Integer.toString (_RequiredTableCols + _OptionalTableCols));
    }

    /**
     * Opens a table element to display a list of items.
     * 
     * @param aRequiredCols
     *            The number of columns that contain required information.
     * 
     * @param aOptionalCols
     *            The number of columns that contain optional information.
     */

    public void openList (int aRequiredCols, int aOptionalCols)
    {
        addTag (_Body, "table", false, "class", "list");
        _RequiredTableCols = aRequiredCols;
        _OptionalTableCols = aOptionalCols;
    }

    @Override
    public void openParagraph ()
    {
        openElement ("<p>");
    }

    public void openRow ()
    {
        addTag (_Body, "tr", false);
        _CellNumber = 0;
    }

    public void openSpan (String aClass)
    {
        openElementEx ("span", "class", aClass);
    }

    public void openTableCell ()
    {
        addTag (_Body, "td", false, "class", (_CellNumber++ >= _RequiredTableCols) ? "optional" : "required");
    }

    /**
     * Sets the title of the document
     * 
     * @param aTitle
     *            The text of the title.
     */

    public void setTitle (String aTitle)
    {
        _Title = aTitle;
    }

    @Override
    public String toString ()
    {
        return getHtml ();
    }
}
